(ns react-native.keychain
  (:require
    ["react-native-keychain" :as react-native-keychain]
    [clojure.string :as string]
    [taoensso.timbre :as log]))

;; ********************************************************************************
;; Storing / Retrieving a user password to/from Keychain
;; ********************************************************************************
;;
;; We are using set/get/reset internet credentials there because they are bound
;; to an address (`server`) property.

(defn enum-val
  [enum-name value-name]
  (get-in (js->clj ^js react-native-keychain) [enum-name value-name]))

;; We need a more strict access mode for keychain entries that save user password.
;; iOS
;;   see this article for more details:
;;   https://developer.apple.com/documentation/security/keychain_services/keychain_items/restricting_keychain_item_accessibility?language=objc
(def keychain-restricted-availability
  ;; From Apple's documentation: > The kSecAttrAccessible attribute enables you to control item
  ;; availability > relative to the lock state of the device. > It also lets you specify eligibility
  ;; for restoration to a new device. > If the attribute ends with the string ThisDeviceOnly, > the
  ;; item can be restored to the same device that created a backup, > but it isn’t migrated when
  ;; restoring another device’s backup data. > ... > For extremely sensitive data > THAT YOU NEVER
  ;; WANT STORED IN iCloud, > you might choose kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly. That
  ;; is exactly what we use there. Note that the password won't be stored if the device isn't locked
  ;; by a passcode.
  #js {:accessible (enum-val "ACCESSIBLE" "WHEN_PASSCODE_SET_THIS_DEVICE_ONLY")})

(def keychain-secure-hardware
  ;; (Android) Requires storing the encryption key for the entry in secure hardware
  ;; or StrongBox (see
  ;; https://developer.android.com/training/articles/keystore#ExtractionPrevention)
  #js {:accessible (enum-val "SECURITY_LEVEL" "SECURE_HARDWARE")})

;; Android only
(defn secure-hardware-available?
  [callback]
  (-> (.getSecurityLevel ^js react-native-keychain)
      (.then (fn [level]
               (callback (= level "SECURE_HARDWARE"))))))

;; iOS only
(defn device-encrypted?
  [callback]
  (-> (.canImplyAuthentication
       ^js react-native-keychain
       (clj->js
        {:authenticationType
         (enum-val "ACCESS_CONTROL" "BIOMETRY_ANY_OR_DEVICE_PASSCODE")}))
      (.then callback)))

(defn save-credentials
  "Stores the credentials for the address to the Keychain"
  ([server username password]
   (save-credentials server username password identity))
  ([server username password callback]
   (when-not (empty? server)
     (-> (.setInternetCredentials ^js react-native-keychain
                                  (string/lower-case server)
                                  username
                                  password
                                  keychain-secure-hardware
                                  keychain-restricted-availability)
         (.then callback)))))

(defn get-credentials
  "Gets the credentials for a specified server from the Keychain"
  ([server]
   (get-credentials server identity))
  ([server callback]
   (when-not (empty? server)
     (-> (.getInternetCredentials ^js react-native-keychain (string/lower-case server))
         (.then callback)))))

(defn reset-credentials
  [server]
  (when-not (empty? server)
    (-> (.resetInternetCredentials ^js react-native-keychain (string/lower-case server))
        (.then #(when-not % (log/error "Error while clearing saved password."))))))
